Conversation
|
Pinging @elastic/kibana-platform |
💔 Build Failed |
| internal: <T extends ResponseError>(err: T, options?: HttpResponseOptions) => | ||
| new KibanaResponse(500, err, options), | ||
| ``` | ||
|
|
There was a problem hiding this comment.
Do these above abstraction methods provide enough value to be worth their existence/DSL/maintenance? It seems like something similar to:
t.respond(options: {
body?: HttpResponsePayload | ResponseError;
statusCode: number;
options?: HttpResponseOptions;
});would be sufficient? Just thinking that most devs are familiar with HTTP, status codes, etc. and the DSL/abstraction may be more trouble than it's worth unless I'm missing context on why it's necessary.
There was a problem hiding this comment.
The only advantage of a DSL that I can think of is much more control over the shape of responses e.g. a 404 can't return its own custom html markup and that a 204 can't return a body. But the value in doing so might be low.
But I agree with what you said an the drawbacks in the rfc:
KibanaResponseToolkitmay not cover all use cases and requires an extension for specific use-cases.
KibanaResponseToolkitoperates low-level Http primitives, such as Headers e.g., and it is not always handy to work with them directly.
There was a problem hiding this comment.
The only advantage of a DSL that I can think of is much more control over the shape of responses e.g. a 404 can't return its own custom html markup and that a 204 can't return a body
right, it's easier for the platform to perform validation and we have some space to extend core functionality in future (for example, add a type definition that WWW-Authenticate header required when a user is rejected as unauthorized).
There was a problem hiding this comment.
I love the idea of a simple DSL for validation & consistency; for me the only thing that's missing is a clear understanding of how the DSL translates to an actual http response payload. (But that might just be me)
IMO that shouldn't prevent us from going this direction -- but to @jasonrhodes' point, many devs are already going to be thinking this way, so making it clear exactly what these methods are doing for them will be important.
There was a problem hiding this comment.
for me the only thing that's missing is a clear understanding of how the DSL translates to an actual http response payload. (But that might just be me)
For success responses, we pass payload to hapi server as is. As I understand, nothing has changed for plugin authors here.
Although, it defines different logic for the error responses. Hapi doesn't allow to respond with Error object, but our DSL does. For the plugin authors it looks like they throw Boom error in hapi request handler.
| internal: <T extends ResponseError>(err: T, options?: HttpResponseOptions) => | ||
| new KibanaResponse(500, err, options), | ||
| ``` | ||
|
|
There was a problem hiding this comment.
The only advantage of a DSL that I can think of is much more control over the shape of responses e.g. a 404 can't return its own custom html markup and that a 204 can't return a body. But the value in doing so might be low.
But I agree with what you said an the drawbacks in the rfc:
KibanaResponseToolkitmay not cover all use cases and requires an extension for specific use-cases.
KibanaResponseToolkitoperates low-level Http primitives, such as Headers e.g., and it is not always handy to work with them directly.
|
|
||
| Other response parameters, such as `etag`, `MIME-type`, `bytes` that used in the Legacy platform could be adjusted via Headers. | ||
|
|
||
| The router handler doesn't expect that logic inside can throw or return something different from `KibanaResponse`. In this case, Http service will respond with `Server error` to prevent exposure of internal logic details. |
There was a problem hiding this comment.
If a handler throws a KibanaResponse, the server should still use this thrown response to populate the actual HTTP response. Any other types of objects thrown should result in a 500.
It's not clear whether or not this is the case in this design.
There was a problem hiding this comment.
A handler function should:
- do not throw an error.
- always return a
KibanaResponse.
There was a problem hiding this comment.
Interesting, so with this approach you are encouraged to construct the response in the handler code itself, rather than allow business logic to construct HTTP responses.
This encourages less coupling of our logic to the HTTP framework, which is a big plus.
// Without allowing exceptions (this RFC)
const async myHandler(context, req, t) {
try {
const obj = await retrieveObj(context.savedObjectClient, req.params.id);
const res = processObj(obj);
return t.ok(res);
} catch (e) {
// handler constructs the error response
return t.notFound();
}
}// Allowing exceptions (not this RFC - more like Hapi)
const async myHandler(context, req, t) {
// business logic construct and throw a 404 on it's own
const obj = await retrieveObj(context.savedObjectClient, req.params.id, t);
const res = processObj(obj);
return t.ok(res);
}There was a problem hiding this comment.
- Throwing and re-throwing errors inside the code are not fun and hard to follow.
- If we mix all exceptions it's hard to type properly and distinguish between business errors & exception raised due to a bug.
- Core doesn't dictate how to handle errors. If a plugin wants to use
Boom- ok, but it's not a part of the core contract anymore. If
I'd write your example as:
const async myHandler(context, req, t) {
const obj = await retrieveObj(context.savedObjectClient, req.params.id);
if (!obj) return t.notFound();
const res = processObj(obj);
return t.ok(res);
}On the other hand, I understand that people are used to the Hapi feature to response with thrown Error and I'm open to introducing this functionality if needed as long as we allow throwing only KibanaResponseError object instead of Boom error.
There was a problem hiding this comment.
I like that exceptions are used for exceptional or unexpected behaviour so I prefer the API of the RFC above the way Hapi does it 👍
There was a problem hiding this comment.
Throwing and re-throwing errors inside the code are not fun and hard to follow.
++ I like how this removes a small element of "magic" and forces you to explicitly respond with errors in the handler itself. To me the approach in this RFC feels simpler, even if it isn't the Hapi way of doing things.
There was a problem hiding this comment.
++, same here, I'd expect "HTTP related logic" (e.g. errors status codes or HTTP headers) to live inside HTTP handlers only where feasible.
There was a problem hiding this comment.
Sounds like we're all on the same page here 🎉 . I just wanted to be clear on how this would work in the NP.
| (context: Context, request: KibanaRequest, t: KibanaResponseToolkit) => { | ||
| // logic to handle request ... | ||
| return t.ok(result); | ||
| ); |
There was a problem hiding this comment.
This may be out of the scope of this RFC.
In previous discussions, we've talked about having more explicit methods for registering routes. For example, we've talked about having a registerPublicApi and a registerInternalApi methods to distinguish between internal and public APIs.
Do we intend these functions to live on the HttpSetup contract and accept a Router or for them to live on the Router object itself?
There was a problem hiding this comment.
In previous discussions, we've talked about having more explicit methods for registering routes. For example, we've talked about having a registerPublicApi and a registerInternalApi methods to distinguish between internal and public APIs.
Where can I find this discussion? I heard about something similar only once #33775 (comment)
I want to structure for myself what considered as public API and internal API? what is system api in this picture?
And yeah, we can create a separate issue to discuss
There was a problem hiding this comment.
We discussed this offline at GAH Orlando as part of the discussion to provide OpenAPI specs for public APIs.
In my view:
- Public APIs are stable across minor releases, provide an OpenAPI spec, are documented publicly, and are exposed to other plugins via a JS client.
- Internal APIs are specific to a plugin and do not provide any stability guarantees.
It's not immediately obvious to me exactly what a "system API" is right now, but it seems like the same concept as an Internal API. I just think it should be more explicit (via the HTTP route instead of a header).
I'll move this to a separate discussion here: #40623
| const kibanaResponseToolkit = { | ||
| // Client errors | ||
| badRequest: <T extends ResponseError>(err: T, options?: HttpResponseOptions) => | ||
| new KibanaResponse(400, err, options), |
There was a problem hiding this comment.
I know Error is an interface, but it's also a class which makes creating instances easy. Maybe there should be a convenience method for constructing instances of ResponseError.
There was a problem hiding this comment.
you mean provide a class or a factory as well?
interface ResponseErrorType extends Error {
meta?: {
data?: JSONValue;
errorCode?: string; // error code to simplify search, translations in i18n, etc.
docLink?: string; // link to the docs
};
}
class ResponseError extends Error implements ResponseErrorType {
constructor(error: Error | string, public readonly meta: ResponseErrorType['meta']) {
super(typeof error === 'string' ? error : error.message);
Object.setPrototypeOf(this, ResponseError.prototype);
}
}
// or a factory
const createError = (error: Error | string, meta?: ResponseErrorType['meta']) =>
new ResponseError(error, meta)There was a problem hiding this comment.
Factory makes sense to me, seems more ergonomic / similar to the response factory methods.
joshdover
left a comment
There was a problem hiding this comment.
LGTM pending couple changes we discussed
- headers are strings - generic --> custom - add responseError factory
| { path: '/url', ...[otherRouteParameters] }, | ||
| (context: Context, request: KibanaRequest, t: KibanaResponseToolkit) => { | ||
| // logic to handle request ... | ||
| return t.ok(result); |
There was a problem hiding this comment.
LGTM
This document could be used as documentation for the route handler.
One thing we were thinking to implement is batching. Batching already exists in Dashboard and Interpreter, but in the NP we were thinking to create a bfetch plugin that would do batching; Dashboard and Interpreter would use it and any other plugin could, too.
The idea is to send a list of HTTP request definitions to the server and then server would "execute" them on the server-side and stream the results back. So, for batching we would need two extra things:
- Be able to "execute" requests internally on the server, without actually going through the wire. For example, response toolkit could have something like
t.execute()that would allow to execute any request locally.
router.post('/bfetch', async (context, request, t) => {
await Promise.all(request.body.requests.map(request => {
return t.execute(request.path, request.body, request.headers);
}));
// ...
);- Then we would need to be able to stream the results back for performance. (This is what now Interpreter does for Canvas functions.) For that we would need ability to write chunks to the socket without closing it. For example,
t.sendHeaders— send headers to clientt.send— send chunk of data to clientt.end— close connection
router.post('/bfetch', async (context, request, t) => {
t.sendHeaders({'Transfer-Encoding': 'chunked'});
await Promise.all((async request => {
const result = await t.execute(request.path, request.body, request.headers);
t.send(result);
}));
t.end();
);|
|
||
| Other response parameters, such as `etag`, `MIME-type`, `bytes` that used in the Legacy platform could be adjusted via Headers. | ||
|
|
||
| The router handler doesn't expect that logic inside can throw or return something different from `KibanaResponse`. In this case, Http service will respond with `Server error` to prevent exposure of internal logic details. |
There was a problem hiding this comment.
Throwing and re-throwing errors inside the code are not fun and hard to follow.
++ I like how this removes a small element of "magic" and forces you to explicitly respond with errors in the handler itself. To me the approach in this RFC feels simpler, even if it isn't the Hapi way of doing things.
| #### KibanaResponseToolkit methods | ||
| Basic primitives: | ||
| ```typescript | ||
| type HttpResponsePayload = undefined | string | JSONValue | Buffer | Stream; |
There was a problem hiding this comment.
I assume a more detailed design of the response payloads for various types of responses is outside the scope of this RFC?
It would be nice to provide some consistency in the payload, for example requiring a message in the payload for a 400 error. The response toolkit DSL would enable us to do this; I'm mostly wondering where/when this discussion would take place. (Or more broadly, how much of this we'd actually plan to enforce in core).
There was a problem hiding this comment.
It would be nice to provide some consistency in the payload, for example requiring a message in the payload for a 400 error. The response toolkit DSL would enable us to do this;
Yeah, our own response toolkit is great, we can tighten signatures as much as we want. Feels like every such case (or group of related cases) would deserve a dedicated PR (not RFC, but at least PR). Another thing to keep in mind is that browsers are full of various quirks and legacy workarounds, so even if we tailor response helpers for the most common use cases we should leave an escape hatch to deal with all that Web weirdness in case it's needed (and I see we have it as custom response toolkit helper, that's great).
There was a problem hiding this comment.
I'd start with more generic definitions outlined in this PR and then tighten them case by case in separate PRs
| internal: <T extends ResponseError>(err: T, options?: HttpResponseOptions) => | ||
| new KibanaResponse(500, err, options), | ||
| ``` | ||
|
|
There was a problem hiding this comment.
I love the idea of a simple DSL for validation & consistency; for me the only thing that's missing is a clear understanding of how the DSL translates to an actual http response payload. (But that might just be me)
IMO that shouldn't prevent us from going this direction -- but to @jasonrhodes' point, many devs are already going to be thinking this way, so making it clear exactly what these methods are doing for them will be important.
azasypkin
left a comment
There was a problem hiding this comment.
LGTM, just few questions/notes.
rfcs/text/0005_route_handler.md
Outdated
| ) => KibanaResponse | Promise<KibanaResponse>; | ||
| ``` | ||
| and accepts next Kibana specific parameters as arguments: | ||
| - context: [Context](https://github.com/elastic/kibana/blob/master/rfcs/text/0003_handler_interface.md). A handler context contains core service and plugin functionality already scoped to the incoming request. |
There was a problem hiding this comment.
optional nit: maybe more specific link to the context description section instead of a link tfull RFC? https://github.com/elastic/kibana/blob/master/rfcs/text/0003_handler_interface.md#handler-context
| *KibanaResponseToolkit* methods allow an end user to adjust the next response parameters: | ||
| - Body. Supported values:`undefined | string | JSONValue | Buffer | Stream`. | ||
| - Status code. | ||
| - Headers. Supports adjusting [known values](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v10/http.d.ts#L8) and attaching [custom values as well](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v10/http.d.ts#L67) |
There was a problem hiding this comment.
note: known values - not really related to the RFC, but wondering if relying on IncomingHttpHeaders gives us much benefits. It can definitely makes our lives a bit harder though (unless we commit to patch it on the Kibana level or contribute to DT typings), e.g. with the current Node typings WWW-Authenticate supports only string while in fact it should support string | string[]. Some browsers (e.g. Firefox) better deal with separate headers, and have quite a few bugs when single WWW-Authenticate header includes multiple comma separated entries even though it's supported by the spec.
There was a problem hiding this comment.
yeah, I noticed it. we can use only names of known headers to improve DX with autocomplete in IDE. on the whole this is optional step and may not implement it.
|
|
||
| Other response parameters, such as `etag`, `MIME-type`, `bytes` that used in the Legacy platform could be adjusted via Headers. | ||
|
|
||
| The router handler doesn't expect that logic inside can throw or return something different from `KibanaResponse`. In this case, Http service will respond with `Server error` to prevent exposure of internal logic details. |
There was a problem hiding this comment.
++, same here, I'd expect "HTTP related logic" (e.g. errors status codes or HTTP headers) to live inside HTTP handlers only where feasible.
| #### KibanaResponseToolkit methods | ||
| Basic primitives: | ||
| ```typescript | ||
| type HttpResponsePayload = undefined | string | JSONValue | Buffer | Stream; |
There was a problem hiding this comment.
It would be nice to provide some consistency in the payload, for example requiring a message in the payload for a 400 error. The response toolkit DSL would enable us to do this;
Yeah, our own response toolkit is great, we can tighten signatures as much as we want. Feels like every such case (or group of related cases) would deserve a dedicated PR (not RFC, but at least PR). Another thing to keep in mind is that browsers are full of various quirks and legacy workarounds, so even if we tailor response helpers for the most common use cases we should leave an escape hatch to deal with all that Web weirdness in case it's needed (and I see we have it as custom response toolkit helper, that's great).
| - `Handler` doesn't cover **all** functionality of the Legacy server at the current moment. For example, we cannot render a view in New platform yet and in this case, we have to proxy the request to the Legacy platform endpoint to perform rendering. All such cases should be considered in an individual order. | ||
| - `KibanaResponseToolkit` may not cover all use cases and requires an extension for specific use-cases. | ||
| - `KibanaResponseToolkit` operates low-level Http primitives, such as Headers e.g., and it is not always handy to work with them directly. | ||
| - `KibanaResponse` cannot be extended with arbitrary data. |
There was a problem hiding this comment.
question: is there any valid use case in KIbana that we treat it as a drawback?
There was a problem hiding this comment.
It's not a drawback per se.
We have custom helper as escape-hatch, but if one doesn't cover a very weird use case it will take some time to extend the core.
we can provide this functionality later. Although, I don't think it's related to
I believe it is covered by accepting router.post('/bfetch', async (context, request, t) => {
const stream = new Stream.PassThrough();
Promise.all(async ()=> {
const result = await core.http.execute(request.path, request.body, request.headers);
stream.write(result);
}).then(() => stream.end());
return res.ok(stream);
}); |
|
thanks everyone for participating, happy we found a common ground 🎉 |
Summary
Related to #33779
View Rendered RFC
[skip-ci]